不管是因為系統設計的問題(開發者所導致的)
又或者是使用者所使用系統的方式超乎常人想像
一個系統多多少少都會出現錯誤與例外
在開發模式下,可能會想要詳細的錯誤頁面來告知開發人員哪邊有例外
但是在生產環境中,這些例外並不適合對外不公開
公開的話就好比告訴別人,這裡是我的弱點,快來攻擊我
以常見的例外找不到資源404 來說,可能會想要將使用者導向特定頁面表示表示資源不存在
至於更嚴重的500 系列錯誤,代表的是系統內部沒有防範到的例外(或刻意為之)
但這類錯誤就不適合公開,讓我們來看看如何處理這類型的例外吧
在建立一個新的web mvc 專案的範本時
其實微軟已經貼心的幫妳加好了
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
內部會去註冊ExceptionHandlerMiddleware
這個例外處理中介軟體
以上面的program.cs
中,只要是開發模式下的例外
都會被導到/Home/Error的自訂頁面
整個Class太長,我們把重點放在invoke 方法上,跟與其相關的HandleExceptionHandlerMiddleware.cs
4`
public class ExceptionHandlerMiddleware
{
public Task Invoke(HttpContext context)
{
ExceptionDispatchInfo edi;
try
{
var task = _next(context);
if (!task.IsCompletedSuccessfully)
{
return Awaited(this, context, task);
}
return Task.CompletedTask;
}
catch (Exception exception)
{
edi = ExceptionDispatchInfo.Capture(exception);
}
return HandleException(context, edi);
static async Task Awaited(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
{
ExceptionDispatchInfo? edi = null;
try
{
await task;
}
catch (Exception exception)
{
edi = ExceptionDispatchInfo.Capture(exception);
}
if (edi != null)
{
await middleware.HandleException(context, edi);
}
}
}
private async Task HandleException(HttpContext context, ExceptionDispatchInfo edi)
{
_logger.UnhandledException(edi.SourceException);
// We can't do anything if the response has already started, just abort.
if (context.Response.HasStarted)
{
_logger.ResponseStartedErrorHandler();
edi.Throw();
}
PathString originalPath = context.Request.Path;
if (_options.ExceptionHandlingPath.HasValue)
{
context.Request.Path = _options.ExceptionHandlingPath;
}
try
{
var exceptionHandlerFeature = new ExceptionHandlerFeature()
{
Error = edi.SourceException,
Path = originalPath.Value!,
Endpoint = context.GetEndpoint(),
RouteValues = context.Features.Get<IRouteValuesFeature>()?.RouteValues
};
ClearHttpContext(context);
context.Features.Set<IExceptionHandlerFeature>(exceptionHandlerFeature);
context.Features.Set<IExceptionHandlerPathFeature>(exceptionHandlerFeature);
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response);
await _options.ExceptionHandler!(context);
if (context.Response.HasStarted || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response)
{
if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled("Microsoft.AspNetCore.Diagnostics.HandledException"))
{
_diagnosticListener.Write("Microsoft.AspNetCore.Diagnostics.HandledException", new { httpContext = context, exception = edi.SourceException });
}
return;
}
edi = ExceptionDispatchInfo.Capture(new InvalidOperationException($"The exception handler configured on {nameof(ExceptionHandlerOptions)} produced a 404 status response. " +
$"This {nameof(InvalidOperationException)} containing the original exception was thrown since this is often due to a misconfigured {nameof(ExceptionHandlerOptions.ExceptionHandlingPath)}. " +
$"If the exception handler is expected to return 404 status responses then set {nameof(ExceptionHandlerOptions.AllowStatusCode404Response)} to true.", edi.SourceException));
}
catch (Exception ex2)
{
// Suppress secondary exceptions, re-throw the original.
_logger.ErrorHandlerException(ex2);
}
finally
{
context.Request.Path = originalPath;
}
edi.Throw(); // Re-throw wrapped exception or the original if we couldn't handle it
}
}
大概的重點是,當原先的pipeline有例外發生的時候
會被catch住,並透過自訂管道處理該例外例外
他會將例外的資訊封裝在ExceptionHandlerFeature
物件並存到HttpContext.Features
並且會將HttpStatusCode設為500
後續自訂處理例外的管道,可以從HttpContext.Features
拿取例外資訊以及原先的路由
這邊給個算是我比較常用的錯誤處理的方式
web api 有好幾種形式,個人比較常接觸的是rest api
rest api 有一個概念是通過HttpStatusCode 來回應使用者相對應的訊息
相較於全部回傳200 ok,並自定義錯誤代碼
REST 的設計活用了http的設計,並給更加語意化的回應
假設在三層式的架構中
使用者所帶入的參數錯誤,可能會需要回傳400的錯誤
如果成功可能會回傳200 的成功
如過不透過ExceptionHandler,或是自訂的錯誤處理Middleware的話
只能在Service層中回傳IHttpResult
這類的狀態,
這樣會讓表現層/框架的東西滲入業務邏輯層,所以我個人不喜歡
但透過ExceptionHandler
加上自訂Exception
,我們可以做到一些有趣的事
我們先自訂一個例外WebApiException.cs
public class WebApiException: Exception
{
public HttpStatusCode StatusCode { get; } = HttpStatusCode.InternalServerError;
public string Msg { get; } = "系統發生錯誤";
public WebApiException(HttpStatusCode statusCode, string msg)
{
StatusCode = statusCode;
Msg = msg;
}
}
StatusCode 表示我們希望使用者看見的HttpStatusCode
Msg 是錯誤訊息
原諒我懶惰,直接把middleware寫在裡面program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseExceptionHandler(b => b.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>()!.Error;
object response;
if (exception is WebApiException httpException)
{
context.Response.StatusCode = (int)httpException.StatusCode;
response = new
{
Message = httpException.Msg
};
}
else
{
response = new
{
Message = "系統發生錯誤"
};
}
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsync(JsonSerializer.Serialize(response));
}));
app.UseHttpsRedirection();
app.UseRouting();
app.MapControllers();
await app.RunAsync();
我們透過ExceptionHandler的middleware
自訂了發生例外後的管道
當例外是我們自訂的WebApiException
時會將回應的HttpStatusCode與訊息設為自訂回應
而其他例外則會回傳500 InternalServerError跟 系統發生錯誤的錯誤訊息
阻絕使用者知道詳細的錯誤訊息並給予一致的回應
通常會搭配log來記錄其他例外,這邊先跳過
我們加個endpoint來看看效果
// ... 省略
app.UseRouting();
app.MapGet("/Test/{id}", ( int id) =>
{
if (id == 1)
{
throw new WebApiException(HttpStatusCode.BadRequest, "錯誤");
}
});
app.MapControllers();
await app.RunAsync();
錯誤跟訊息如我們想像,回傳了BadRequest,跟自訂訊息
必須承認,後面的文章其實都有點混
每年都說要提早準備
結果都是每天趕文章
希望明年能夠好好準備一篇真正有價值的主題